<?php
// +----------------------------------------------------------------------
// | 云静资源
// +----------------------------------------------------------------------
// | Copyright (c) 2019-2022 http://www.iyunj.cn
// +----------------------------------------------------------------------
// | Author: Uncle-L <1071446619@qq.com>
// +----------------------------------------------------------------------
// | 公共函数库
// +----------------------------------------------------------------------

/****************************************************** 数组 ******************************************************/

if (!function_exists('array_eq')) {
    /**
     * Notes: 判断两个数组的长度、键值对是否一一对应且相等
     * Author: Uncle-L
     * Date: 2020/10/26
     * Time: 16:56
     * @param array $arr1
     * @param array $arr2
     * @return bool
     */
    function array_eq(array $arr1 = [], array $arr2 = []): bool {
        if (count($arr1) != count($arr2)) return false;
        foreach ($arr1 as $k => $v) {
            if (!isset($arr2[$k]) || $arr2[$k] != $v) return false;
        }
        return true;
    }
}

if (!function_exists('array_in')) {
    /**
     * Notes: 判断数组值是否在指定规则数组内（只针对一维数组）
     * Author: Uncle-L
     * Date: 2020/2/17
     * Time: 14:19
     * @param array $arr [待判断数组]
     * @param array $rule_arr [规则数组]
     * @return bool
     */
    function array_in(array $arr = [], array $rule_arr = []): bool {
        if (!$rule_arr) return false;
        foreach ($arr as $v) {
            if (!in_array($v, $rule_arr)) return false;
        }
        return true;
    }
}

if (!function_exists('array_supp')) {
    /**
     * Notes: 数组补齐
     * Author: Uncle-L
     * Date: 2020/3/3
     * Time: 18:47
     * @param array $arr [待补齐数组]
     * @param array $rule_arr [规则数组]
     * @return array|bool
     */
    function array_supp(array $arr = [], array $rule_arr = []) {
        if (!$arr || !is_array($arr)) return $rule_arr;
        foreach ($rule_arr as $rule_k => $rule_v) {
            if (!isset($arr[$rule_k])) continue;
            $v = $arr[$rule_k];
            if ($rule_v && is_array($rule_v)) {
                // 如果和合并元素
                if (!is_array($v)) continue;
                if (!$rule_v || is_regular_arr($rule_v)) {
                    $v += $rule_v;
                } else {
                    $v = array_supp($v, $rule_v);
                }
            }
            $rule_arr[$rule_k] = $v;
        }
        return $rule_arr;
    }
}

if (!function_exists('array_depth')) {
    /**
     * Notes: 获取/判断数组维度
     * Author: Uncle-L
     * Date: 2020/6/28
     * Time: 18:04
     * @param array $arr
     * @param int $rule_depth [规则维度，不传则返回数组维度|传入则校验数组是否该维度]
     * @return int|bool
     */
    function array_depth(array $arr = [], int $rule_depth = -1) {
        $arrs = new RecursiveIteratorIterator(new RecursiveArrayIterator($arr));
        $depth = 0;
        foreach ($arrs as $v)
            $arrs->getDepth() >= $depth and $depth = $arrs->getDepth();
        return is_positive_integer($rule_depth) ? ++$depth === $rule_depth : ++$depth;
    }
}

if (!function_exists('array_insert')) {
    /**
     * Notes: 在数组指定key前插入数组
     * Author: Uncle-L
     * Date: 2020/7/18
     * Time: 23:46
     * @param array $arr [目标数组]
     * @param array $data [待插入数组]
     * @param bool|mixed $key [指定key]
     * @return array
     */
    function array_insert(array &$arr = [], array $data = [], $key = false): array {
        if (!is_array($data)) return $arr;
        $key = $key === false ? key($arr) : $key;
        if (!isset($arr[$key])) return $arr;
        $arrHead = [];
        $arrFoot = [];
        $headSliceEnd = false;
        foreach ($arr as $k => $v) {
            if (!$headSliceEnd) {
                if ($k == $key) {
                    $headSliceEnd = true;
                    $arrFoot[$k] = $v;
                } else {
                    $arrHead[$k] = $v;
                }
            } else {
                $arrFoot[$k] = $v;
            }
        }
        $arr = $arrHead + $data + $arrFoot;
        return $arr;
    }
}

if (!function_exists('array_key_prefix')) {
    /**
     * Notes: 给数组所有key或指定key增加前缀
     * Author: Uncle-L
     * Date: 2021/1/8
     * Time: 16:37
     * @param array $arr
     * @param string $prefix [前缀]
     * @param bool|mixed $key [指定key，注意：若新key在原数组中存在则前缀翻倍再计算]
     * @return array
     */
    function array_key_prefix(array &$arr, string $prefix, $key = false): array {
        if (!$arr || !$prefix) return $arr;
        if ($key) {
            if (!array_key_exists($key, $arr)) return $arr;
            $newKey = $prefix . $key;
            if (array_key_exists($newKey, $arr)) return array_key_prefix($arr, $prefix . $prefix, $key);
            $arr[$newKey] = $arr[$key];
            unset($arr[$key]);
            return $arr;
        }
        $arr = array_combine(array_map(function ($key) use ($prefix) {
            return $prefix . $key;
        }, array_keys($arr)), $arr);
        return $arr;
    }
}

if (!function_exists('array_not_key')) {
    /**
     * Notes: 数组没有特定声明的key，即常规的没有指定key的数组，例：["apple","pear","orange"]
     * Author: Uncle-L
     * Date: 2021/11/7
     * Time: 23:17
     * @param array $arr
     * @return bool
     */
    function array_not_key(array $arr): bool {
        if (!$arr) return true;
        $keys = array_keys($arr);
        if (end($keys) !== (count($keys) - 1)) return false;
        if (reset($keys) !== 0) return false;
        for ($i = 0; $i < count($keys); $i++)
            if ($i !== $keys[$i]) return false;
        return true;
    }
}

if (!function_exists('array_value_short')) {
    /**
     * 数组元素值缩短
     * @param array $arr
     * @param int $max_len 参数允许最大长度
     * @return array
     */
    function array_value_short(array $arr, int $max_len = 1000): array {
        foreach ($arr as &$v) {
            if (is_string($v) && $v && ($vLen = mb_strlen($v, 'utf-8')) > $max_len) {
                // 判断是否为json
                if (is_json($v, $vData, true)) {
                    $v = json_encode(array_value_short($vData, $max_len), JSON_UNESCAPED_UNICODE);
                } else {
                    $len = $max_len > 20 ? 20 : $max_len;
                    $v = mb_substr($v, 0, $len, 'utf-8') . '...';
                }
            }
            if (is_array($v)) {
                $v = array_value_short($v, $max_len);
            }
        }
        return $arr;
    }
}

if (!function_exists('array_sort')) {
    /**
     * 二维数组指定某一列进行排序
     * @param array $arr
     * @param string $key 排序列key
     * @param string $order 排序（asc升序|desc降序）
     * @return array
     */
    function array_sort(array $arr, string $key = 'sort', string $order = 'asc'): array {
        if ($arr) {
            $keys = array_column($arr, $key);
            array_multisort($keys, $order == 'asc' ? SORT_ASC : SORT_DESC, $arr);
        }
        return $arr;
    }
}

/****************************************************** 验证 ******************************************************/

if (!function_exists('is_mobile')) {
    /**
     * Notes: 是否移动端判断
     * Author: Uncle-L
     * Date: 2020/2/19
     * Time: 9:21
     * @return bool [是移动端返回true|反之]
     */
    function is_mobile(): bool {
        $_SERVER['ALL_HTTP'] = isset($_SERVER['ALL_HTTP']) ?? '';
        $mobile_browser = '0';
        if (preg_match("/(up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone|iphone|ipad|ipod|android|xoom)/i", strtolower($_SERVER['HTTP_USER_AGENT'])))
            $mobile_browser++;
        if ((isset($_SERVER['HTTP_ACCEPT'])) and (strpos(strtolower($_SERVER['HTTP_ACCEPT']), 'application/vnd.wap.xhtml+xml') !== false))
            $mobile_browser++;
        if (isset($_SERVER['HTTP_X_WAP_PROFILE']))
            $mobile_browser++;
        if (isset($_SERVER['HTTP_PROFILE']))
            $mobile_browser++;
        $mobile_ua = strtolower(substr($_SERVER['HTTP_USER_AGENT'], 0, 4));
        $mobile_agents = array(
            'w3c ', 'acs-', 'alav', 'alca', 'amoi', 'audi', 'avan', 'benq', 'bird', 'blac',
            'blaz', 'brew', 'cell', 'cldc', 'cmd-', 'dang', 'doco', 'eric', 'hipt', 'inno',
            'ipaq', 'java', 'jigs', 'kddi', 'keji', 'leno', 'lg-c', 'lg-d', 'lg-g', 'lge-',
            'maui', 'maxo', 'midp', 'mits', 'mmef', 'mobi', 'mot-', 'moto', 'mwbp', 'nec-',
            'newt', 'noki', 'oper', 'palm', 'pana', 'pant', 'phil', 'play', 'port', 'prox',
            'qwap', 'sage', 'sams', 'sany', 'sch-', 'sec-', 'send', 'seri', 'sgh-', 'shar',
            'sie-', 'siem', 'smal', 'smar', 'sony', 'sph-', 'symb', 't-mo', 'teli', 'tim-',
            'tosh', 'tsm-', 'upg1', 'upsi', 'vk-v', 'voda', 'wap-', 'wapa', 'wapi', 'wapp',
            'wapr', 'webc', 'winw', 'winw', 'xda', 'xda-'
        );
        if (in_array($mobile_ua, $mobile_agents))
            $mobile_browser++;
        if (strpos(strtolower($_SERVER['ALL_HTTP']), 'operamini') !== false)
            $mobile_browser++;
        // win
        if (strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'windows') !== false)
            $mobile_browser = 0;
        // win7
        if (strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'windows phone') !== false)
            $mobile_browser++;
        if ($mobile_browser > 0) {
            return true;
        }
        return false;
    }
}

if (!function_exists('is_positive_int')) {
    /**
     * Notes: 正整数验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @return bool
     */
    function is_positive_int($data): bool {
        return is_scalar($data) && !!preg_match("/^[1-9]\d*$/", $data);
    }
}

if (!function_exists('is_positive_integer')) {
    /**
     * Notes: 正整数验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @return bool [是正整数返回true|反之]
     */
    function is_positive_integer($data): bool {
        return is_numeric($data) && is_int($data + 0) && ($data + 0) > 0;
    }
}

if (!function_exists('is_nonnegative_int')) {
    /**
     * Notes: 非负整数验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @return bool
     */
    function is_nonnegative_int($data): bool {
        return is_scalar($data) && !!preg_match("/^([1-9]\d*|0)$/", $data);
    }
}

if (!function_exists('is_nonnegative_integer')) {
    /**
     * Notes: 非负整数验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @return bool
     */
    function is_nonnegative_integer($data): bool {
        return is_numeric($data) && is_int($data + 0) && ($data + 0) >= 0;
    }
}

if (!function_exists('is_nonnegative_num')) {
    /**
     * Notes: 非负数验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @return bool
     */
    function is_nonnegative_num($data): bool {
        return is_scalar($data) && !!preg_match("/^(0|[1-9]\d*)(\.\d+)?$/", $data);
    }
}

if (!function_exists('is_timestamp')) {
    /**
     * Notes: 时间戳验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @return bool
     */
    function is_timestamp($data): bool {
        return is_positive_int($data) && $data == strtotime(date("Y-m-d H:i:s", $data));
    }
}

if (!function_exists('is_json')) {
    /**
     * Notes: json数据格式验证
     * Author: Uncle-L
     * Date: 2020/3/21
     * Time: 17:38
     * @param mixed $data
     * @param mixed $decodeData 接收decode后的数据
     * @param bool $associative decode是否转换为数组
     * @return bool [是json数据格式返回true|反之返回false]
     */
    function is_json($data, &$decodeData = null, bool $associative = false): bool {
        if (!$data || !is_string($data)) return false;
        $decodeData = json_decode($data, $associative);
        return json_last_error() == JSON_ERROR_NONE;
    }
}

if (!function_exists('is_date')) {
    /**
     * 日期有效性验证
     * @param mixed $data [待校验的时间日期]
     * @return bool
     */
    function is_date($data): bool {
        return false !== strtotime($data);
    }
}

if (!function_exists('is_datetime')) {
    /**
     * Notes: 时间日期有效性验证
     * Author: Uncle-L
     * Date: 2020/5/26
     * Time: 16:53
     * @param mixed $data [待校验的时间日期]
     * @return bool
     */
    function is_datetime($data): bool {
        $timestamp = strtotime($data);
        if (!$timestamp) return false;
        return $timestamp && (date("s", $timestamp) === substr($data, -2));
    }
}

if (!function_exists('is_year')) {
    /**
     * Notes: 年份有效性验证
     * Author: Uncle-L
     * Date: 2020/5/26
     * Time: 16:53
     * @param mixed $data
     * @return bool
     */
    function is_year($data): bool {
        return is_positive_int($data);
    }
}

if (!function_exists('is_year_month')) {
    /**
     * Notes: 年月有效性验证（如：2019-09）
     * Author: Uncle-L
     * Date: 2020/5/26
     * Time: 16:53
     * @param mixed $data
     * @return bool
     */
    function is_year_month($data): bool {
        if (!is_string($data)) return false;
        $time = strtotime($data);
        return date('m', $time) === substr($data, -2);
    }
}

if (!function_exists('is_month')) {
    /**
     * Notes: 月份有效性验证
     * Author: Uncle-L
     * Date: 2020/5/26
     * Time: 16:53
     * @param mixed $data
     * @return bool
     */
    function is_month($data): bool {
        if (!is_positive_int($data)) return false;
        $data = (int)$data;
        return $data >= 1 && $data <= 12;
    }
}

if (!function_exists('is_time')) {
    /**
     * Notes: 时间有效性验证(01:01:01)
     * Author: Uncle-L
     * Date: 2020/5/26
     * Time: 16:53
     * @param mixed $data
     * @return bool
     */
    function is_time($data): bool {
        if (!is_string($data)) return false;
        $data = "2019-06-15 {$data}";
        $time = strtotime($data);
        return $time && date("s", $time) === substr($data, -2);
    }
}

if (!function_exists('is_md5_result')) {
    /**
     * Notes: 判断是否为md5加密结果
     * Author: Uncle-L
     * Date: 2021/7/2
     * Time: 17:19
     * @param $result
     * @param int $len
     * @return bool
     */
    function is_md5_result($result, int $len = 32): bool {
        if (!is_string($result) || !$result || strlen($result) != $len) return false;
        return !!preg_match('/^[a-z0-9]{' . $len . '}$/', $result);
    }
}

if (!function_exists('is_positive_int_array')) {
    /**
     * Notes: 判断数组是否为正整数数组
     * Author: Uncle-L
     * Date: 2021/11/15
     * Time: 15:00
     * @param mixed $data
     * @return bool
     */
    function is_positive_int_array($data): bool {
        if (!is_array($data) || !$data) return false;
        $arrStr = implode(",", $data);
        return preg_match("/^[1-9]\d*(?:,[1-9]\d*)*$/", $arrStr);
    }
}

if (!function_exists('is_valid_ip')) {
    /**
     * 判断是否是合法的IP地址
     * @param string $ip IP地址
     * @param string $type IP地址类型 (ipv4, ipv6)
     * @return bool
     */
    function is_valid_ip($ip, string $type = ''): bool {
        if (!is_string($ip) || $ip == '') return false;
        switch (strtolower($type)) {
            case 'ipv4':
                $flag = FILTER_FLAG_IPV4;
                break;
            case 'ipv6':
                $flag = FILTER_FLAG_IPV6;
                break;
            default:
                $flag = 0;
                break;
        }

        return boolval(filter_var($ip, FILTER_VALIDATE_IP, $flag));
    }
}

if (!function_exists('is_url')) {
    /**
     * 判断是否是合法的url地址
     * @param mixed $data
     * @return bool
     */
    function is_url($data): bool {
        if (!is_string($data) || $data == '') return false;
        $pattern = '/^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+/';
        return !!preg_match($pattern, $data);
    }
}

if (!function_exists('is_uri')) {
    /**
     * 判断是否是合法的uri地址，/开头
     * @param mixed $data
     * @return bool
     */
    function is_uri($data): bool {
        if (!is_string($data) || $data == '') return false;
        $pattern = '/^\/[^\s]+/';
        return !!preg_match($pattern, $data);
    }
}

if (!function_exists('is_regular_arr')) {
    /**
     * 判断是否是常规数组
     * array_values会返回一个数组中所有的值，并重新索引数字键。
     * 如果原始数组是常规数组，array_values将返回相同的数组。
     * 如果原始数组是键值对数组，它将重新索引数字键，返回一个常规数组。
     * @param mixed $data
     * @return bool
     */
    function is_regular_arr($data): bool {
        return is_array($data) && $data === array_values($data);
    }
}

/****************************************************** 时间日期 ******************************************************/

if (!function_exists('msectime')) {
    /**
     * Notes: 获取当前毫秒时间戳
     * Author: Uncle-L
     * Date: 2020/11/23
     * Time: 11:35
     * @return int
     */
    function msectime(): int {
        list($msec, $sec) = explode(' ', microtime());
        $msectime = (float)sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
        return (int)substr($msectime, 0, 13);
    }
}

if (!function_exists('msectime_to_datetime')) {
    /**
     * 毫秒时间戳转日期时间
     * @param int|string|mixed $msectime 毫秒时间戳
     * @param bool $has_ms 是否包含毫秒部分
     * @return string
     */
    function msectime_to_datetime($msectime, bool $has_ms = true): string {
        $timeSeconds = floor($msectime / 1000);
        if ($has_ms) {
            $timeMilliseconds = $msectime % 1000;
            return date('Y-m-d H:i:s', $timeSeconds) . '.' . $timeMilliseconds;
        } else {
            return date('Y-m-d H:i:s', $timeSeconds);
        }
    }
}

/****************************************************** 文件 ******************************************************/

if (!function_exists('ds_replace')) {
    /**
     * Notes: 目录分隔符替换
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:15
     * @param string $path [目录地址]
     * @return bool
     */
    function ds_replace(string $path): string {
        $target = DIRECTORY_SEPARATOR === "/" ? "\\" : "/";
        return str_replace($target, DIRECTORY_SEPARATOR, $path);
    }
}

if (!function_exists('dir_delete')) {
    /**
     * Notes: 目录删除
     * Author: Uncle-L
     * Date: 2021/11/7
     * Time: 13:11
     * @param string $path [要删除的目录地址]
     * @param bool $inc_self [是否包含自身]
     * @param array ...$exclude_path 排除地址
     */
    function dir_delete(string $path, bool $inc_self = false, ...$exclude_path): void {
        if (!is_dir($path)) return;
        $path = ds_replace($path);
        if (substr($path, -1) === DIRECTORY_SEPARATOR) $path = substr($path, 0, -1);
        $fileArr = scandir($path);
        $excludePath = isset($exclude_path[0]) && is_array($exclude_path[0]) ? $exclude_path[0] : $exclude_path;
        $excludePath = array_map(function ($v) use ($path) {
            $v = ds_replace($v);
            return str_replace($path . DIRECTORY_SEPARATOR, "", $v);
        }, $excludePath);
        foreach ($fileArr as $file) {
            if ($file === "." || $file === "..") continue;
            $filePath = $path . DIRECTORY_SEPARATOR . $file;
            if (is_file($filePath)) {
                if (in_array($file, $excludePath)) continue;
                @unlink($filePath);
            } else {
                dir_delete($filePath, true, $excludePath);
            }
        }
        if ($inc_self) @rmdir($path);
    }
}

if (!function_exists('file_copy')) {
    /**
     * Notes: 文件复制
     * Author: Uncle-L
     * Date: 2021/11/7
     * Time: 14:01
     * @param string $source_path [源地址]
     * @param string $dest_path [目标地址]
     */
    function file_copy(string $source_path, string $dest_path): void {
        $sourcePath = ds_replace($source_path);
        $destPath = ds_replace($dest_path);
        if (!is_file($sourcePath)) return;
        $destDirPath = dirname($destPath);
        if (is_dir($destDirPath)) {
            $isWriteable = file_is_writeable($destDirPath);
            if (!$isWriteable) throw new \RuntimeException("文件夹[{$destDirPath}]无写入权限");
        } else
            try {
                mkdir($destDirPath, 0755, true);
            } catch (\Exception $e) {
                throw new \RuntimeException("文件夹[{$destDirPath}]不存在，请创建并赋予写入权限后重新执行此操作");
            }
        copy($sourcePath, $destPath);
    }
}

if (!function_exists('dir_copy')) {
    /**
     * Notes: 目录复制
     * Author: Uncle-L
     * Date: 2021/11/7
     * Time: 14:01
     * @param string $source_path [源地址]
     * @param string $dest_path [目标地址]
     * @param array ...$exclude_path 排除地址
     */
    function dir_copy(string $source_path, string $dest_path, ...$exclude_path): void {
        $sourcePath = ds_replace($source_path);
        if (substr($sourcePath, -1) === DIRECTORY_SEPARATOR) $sourcePath = substr($sourcePath, 0, -1);
        $destPath = ds_replace($dest_path);
        if (substr($destPath, -1) === DIRECTORY_SEPARATOR) $destPath = substr($destPath, 0, -1);
        if (!is_dir($sourcePath)) return;
        if (!is_dir($destPath)) mkdir($destPath, 0755, true);
        $sourceFileArr = scandir($sourcePath);
        $excludePath = isset($exclude_path[0]) && is_array($exclude_path[0]) ? $exclude_path[0] : $exclude_path;
        $excludePath = array_map(function ($v) use ($sourcePath) {
            $v = ds_replace($v);
            return str_replace($sourcePath . DIRECTORY_SEPARATOR, "", $v);
        }, $excludePath);
        foreach ($sourceFileArr as $sourceFile) {
            if ($sourceFile === "." || $sourceFile === "..") continue;
            $sourceFilePath = $sourcePath . DIRECTORY_SEPARATOR . $sourceFile;
            $destFilePath = $destPath . DIRECTORY_SEPARATOR . $sourceFile;
            if ($excludePath && in_array($sourceFile, $excludePath)) continue;
            if (is_file($sourceFilePath)) {
                file_copy($sourceFilePath, $destFilePath);
            } else {
                dir_copy($sourceFilePath, $destFilePath);
            }
        }
    }
}

if (!function_exists('file_is_writeable')) {
    /**
     * Notes: 文件/目录 是否可写（取代系统自带的 is_writeable 函数）
     * Author: Uncle-L
     * Date: 2021/11/10
     * Time: 10:14
     * @param string $path [文件/目录 地址]
     * @return bool
     */
    function file_is_writeable(string $path): bool {
        $path = ds_replace($path);
        if (substr($path, -1) === DIRECTORY_SEPARATOR) $path = substr($path, 0, -1);
        if (is_dir($path)) {
            $dir = $path;
            $testFile = $dir . DIRECTORY_SEPARATOR . "test_is_writable_" . date("YmdHis") . ".txt";
            if ($fp = @fopen($testFile, 'w')) {
                @fclose($fp);
                @unlink($testFile);
                $isWiteable = true;
            } else {
                $isWiteable = false;
            }
        } else {
            if ($fp = @fopen($path, 'a+')) {
                @fclose($fp);
                $isWiteable = true;
            } else {
                $isWiteable = false;
            }
        }
        return $isWiteable;
    }
}

if (!function_exists('dir_is_writeable')) {
    /**
     * Notes: 目录是否可写
     * Author: Uncle-L
     * Date: 2021/11/7
     * Time: 14:01
     * @param string $path [目录地址]
     * @return bool
     */
    function dir_is_writeable(string $path): bool {
        $path = ds_replace($path);
        if (substr($path, -1) === DIRECTORY_SEPARATOR) $path = substr($path, 0, -1);
        if (!is_dir($path)) return false;
        $isWriteable = file_is_writeable($path);
        if (!$isWriteable) return false;
        $fileArr = scandir($path);
        foreach ($fileArr as $file) {
            if ($file === "." || $file === "..") continue;
            $filePath = $path . DIRECTORY_SEPARATOR . $file;
            if (is_file($file)) continue;
            $isWriteable = file_is_writeable($filePath);
            if (!$isWriteable) return false;
        }
        return true;
    }
}

if (!function_exists('dir_writeable_mk')) {
    /**
     * Notes: 目录是否可写，不存在则创建
     * 目录存在：
     *      可写：结束执行
     *      不可写：抛出异常提示
     * 目录不存在：
     *      创建，失败则抛出异常提示
     * Author: Uncle-L
     * Date: 2021/11/7
     * Time: 14:01
     * @param string $path [目录地址]
     * @param bool $recursion [递归，是否递归检查子目录]
     */
    function dir_writeable_mk(string $path, bool $recursion = false): void {
        $path = ds_replace($path);
        if (substr($path, -1) === DIRECTORY_SEPARATOR) $path = substr($path, 0, -1);
        if (is_dir($path)) {
            $isWritable = $recursion ? dir_is_writeable($path) : file_is_writeable($path);
            if (!$isWritable) throw new \RuntimeException("请赋予目录[{$path}]写入权限");
            return;
        } else
            try {
                mkdir($path, 0755, true);
            } catch (\Exception $e) {
                throw new \RuntimeException("请创建目录[{$path}]，并赋予其写入权限");
            }
    }
}

if (!function_exists('is_empty_dir')) {
    /**
     * 判断是否为空目录
     * @param string $path
     * @return bool
     */
    function is_empty_dir(string $path): bool {
        $path = ds_replace($path);
        if (substr($path, -1) === DIRECTORY_SEPARATOR) $path = substr($path, 0, -1);
        if (!is_dir($path)) throw new \RuntimeException("请先创建目录[{$path}]");
        $fileArr = scandir($path);
        foreach ($fileArr as $file) {
            if ($file === "." || $file === "..") continue;
            return false;
        }
        return true;
    }
}

/****************************************************** 字符串 ******************************************************/

if (!function_exists('rand_char')) {
    /**
     * Notes: 获取指定长度随机字符串
     * Author: Uncle-L
     * Date: 2020/11/23
     * Time: 11:38
     * @param int $len [默认长度32]
     * @param string $char_pool [字符池]
     * @return string
     */
    function rand_char(int $len = 32, string $char_pool = ''): string {
        $str = "";
        $charPool = $char_pool ?: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
        $max = strlen($charPool) - 1;
        for ($i = 0; $i < $len; $i++) {
            $str .= $charPool[rand(0, $max)];
        }
        return $str;
    }
}

if (!function_exists('filter_sql')) {
    /**
     * Notes: 过滤sql，防止sql注入
     * Author: Uncle-L
     * Date: 2021/1/5
     * Time: 18:27
     * @param string $str
     * @return string
     */
    function filter_sql(string $str): string {
        if (!$str) return '';
        $str = addslashes($str);
        $str = str_replace("_", "\_", $str);
        $str = str_replace("%", "\%", $str);
        $str = str_replace(" ", "", $str);
        $str = nl2br($str); // 回车转换
        return htmlspecialchars($str);
    }
}

if (!function_exists('start_with')) {
    /**
     * Notes: 检测字符串是否以指定的字符串开始/指定位置开始
     * Author: Uncle-L
     * Date: 2021/2/4
     * Time: 10:27
     * @param string $str [要检查的字符串]
     * @param string $pattern [指定字符串]
     * @param int $idx [开始位置]
     * @return bool
     */
    function start_with(string $str, string $pattern, int $idx = 0): bool {
        return strpos($str, $pattern) === $idx;
    }
}

if (!function_exists('strnpos')) {
    /**
     * Notes: 找字符串在另一字符串中第n次出现的位置
     * Author: Uncle-L
     * Date: 2021/11/18
     * Time: 15:19
     * @param string $str
     * @param string $find
     * @param int $n
     * @return int|false
     */
    function strnpos(string $str, string $find, int $n) {
        $posVal = false;
        for ($i = 1; $i <= $n; $i++) {
            $pos = strpos($str, $find);
            $str = substr($str, $pos + 1);
            $posVal = $pos + ($posVal === false ? 0 : $posVal) + 1;
        }
        return $posVal ? ($posVal - 1) : false;
    }
}

if (!function_exists('str_replace_nth')) {
    /**
     * 查找字符串中第几次出现的字符串，并替换为指定字符串
     *
     * @param string|mixed $str     字符串
     * @param string|mixed $search  要查找的字符串
     * @param string|mixed $replace 要替换的结果字符串
     * @param int $nth  第几次
     * @return void
     */
    function str_replace_nth(string $str, string $search, string $replace, int $nth = 1): string {
        $searchLen = mb_strlen($search, 'UTF-8');
        $count = 0;
        $start = 0;
        $result = '';
     
        while (($pos = mb_strpos($str, $search, $start, 'UTF-8')) !== false) {
            $count++;
            if ($count == $nth) {
                // 拼接替换前的部分
                $result .= mb_substr($str, 0, $pos, 'UTF-8') . $replace;
                // 查找替换后的剩余部分并拼接
                $result .= mb_substr($str, $pos + $searchLen, null, 'UTF-8');
                break;
            }
            // 移动起始位置到当前查找位置的后面
            $start = $pos + $searchLen;
        }
     
        // 如果没有找到第 N 次出现，则返回原始字符串
        if ($count < $nth) {
            $result = $str;
        }
     
        return $result;
    }
}

if (!function_exists('uppercase_to_underline')) {
    /**
     * Notes: 大写格式转换为下划线分割（兼容首字母大小写情况）
     * Author: Uncle-L
     * Date: 2021/4/15
     * Time: 14:04
     * @param string $str
     * @return string
     */
    function uppercase_to_underline(string $str): string {
        return strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $str));
    }
}

if (!function_exists('underline_to_uppercase')) {
    /**
     * Notes: 下划线转换为大写格式
     * Author: Uncle-L
     * Date: 2021/4/15
     * Time: 14:04
     * @param string $str
     * @param bool $has_first 是否首字母大写
     * @return string
     */
    function underline_to_uppercase(string $str, bool $has_first = false): string {
        $strArr = explode('_', $str);
        $res = $has_first ? '' : $strArr[0];
        for ($i = $has_first ? 0 : 1, $l = count($strArr); $i < $l; $i++) {
            $res .= ucfirst($strArr[$i]);
        }
        return $res;
    }
}

if (!function_exists('merge_spaces')) {
    /**
     * 多个连续空格只保留一个
     * @param string|mixed $str
     * @return string|mixed
     */
    function merge_spaces($str) {
        return $str && is_string($str) ? preg_replace("/\s(?=\s)/", "\\1", $str) : $str;
    }
}

if (!function_exists('calling_class')) {
    /**
     * Notes: 获取调用此方法的类名
     * Author: Uncle-L
     * Date: 2021/4/15
     * Time: 14:15
     * @return mixed
     */
    function calling_class() {
        $trace = debug_backtrace();
        $class = $trace[1]['class'];

        for ($i = 1; $i < count($trace); $i++) {
            if (isset($trace[$i]))
                if ($class != $trace[$i]['class'])
                    return $trace[$i]['class'];
        }
    }
}

if (!function_exists('fuzzy_mobile')) {
    /**
     * Notes: 模糊手机号码（num=<7直接输出，7<num<11前后各保留2位，num>=11前后各保留3、4位，其余字符用*代替）
     * Author: Uncle-L
     * Date: 2021/4/28
     * Time: 11:38
     * @param string|int $num
     * @return string|int
     */
    function fuzzy_mobile($num) {
        $len = strlen($num);
        if ($len <= 7) return $num;
        list($fuzzyStartDigit, $fuzzyLen) = $len < 11 ? [3, 3] : [3, 4];

        $fuzzyData = mb_substr($num, 0, $fuzzyStartDigit, 'utf8')
            . str_repeat("*", $fuzzyLen)
            . mb_substr($num, $fuzzyStartDigit + $fuzzyLen, null, 'utf8');
    }
}

if (!function_exists('pinyin_first_letter')) {
    /**
     * Notes: 获取字符串拼音的第一个字母（大写），不是字母返回空字符串
     * Author: Uncle-L
     * Date: 2021/11/8
     * Time: 14:04
     * @param string $str
     * @return string
     */
    function pinyin_first_letter(string $str): string {
        $str = ltrim($str);
        if (!$str) return "";
        $str = mb_substr($str, 0, 2, "UTF-8");
        $fchar = ord($str[0]);
        if ($fchar >= ord('A') && $fchar <= ord('z')) return strtoupper($str[0]);
        $s1 = iconv('UTF-8', 'GB2312//IGNORE', $str);   // IGNORE 避免繁体字报错
        $s2 = iconv('GB2312//IGNORE', 'UTF-8', $s1);
        $s = $s2 == $str ? $s1 : $str;
        $asc = ord($s[0]) * 256 + ord($s[1]) - 65536;
        if ($asc >= -20319 && $asc <= -20284) return 'A';
        if ($asc >= -20283 && $asc <= -19776) return 'B';
        if ($asc >= -19775 && $asc <= -19219) return 'C';
        if ($asc >= -19218 && $asc <= -18711) return 'D';
        if ($asc >= -18710 && $asc <= -18527) return 'E';
        if ($asc >= -18526 && $asc <= -18240) return 'F';
        if ($asc >= -18239 && $asc <= -17923) return 'G';
        if ($asc >= -17922 && $asc <= -17418) return 'H';
        if ($asc >= -17417 && $asc <= -16475) return 'J';
        if ($asc >= -16474 && $asc <= -16213) return 'K';
        if ($asc >= -16212 && $asc <= -15641) return 'L';
        if ($asc >= -15640 && $asc <= -15166) return 'M';
        if ($asc >= -15165 && $asc <= -14923) return 'N';
        if ($asc >= -14922 && $asc <= -14915) return 'O';
        if ($asc >= -14914 && $asc <= -14631) return 'P';
        if ($asc >= -14630 && $asc <= -14150) return 'Q';
        if ($asc >= -14149 && $asc <= -14091) return 'R';
        if ($asc >= -14090 && $asc <= -13319) return 'S';
        if ($asc >= -13318 && $asc <= -12839) return 'T';
        if ($asc >= -12838 && $asc <= -12557) return 'W';
        if ($asc >= -12556 && $asc <= -11848) return 'X';
        if ($asc >= -11847 && $asc <= -11056) return 'Y';
        if ($asc >= -11055 && $asc <= -10247) return 'Z';
        return "";
    }
}

if (!function_exists('aes_encrypt')) {
    /**
     * Notes: AES数据加密
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:07
     * @param string $data [源数据]
     * @param string $key [16字节密钥]
     * @param string $iv [16字节初始向量]
     * @return string
     */
    function aes_encrypt(string $data, string $key, string $iv): string {
        return base64_encode(openssl_encrypt($data, "AES-128-CBC", $key, 0, $iv));
    }
}

if (!function_exists('aes_decrypt')) {
    /**
     * Notes: AES数据解密
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:07
     * @param string $encrypted [密文]
     * @param string $key [16字节密钥]
     * @param string $iv [16字节初始向量]
     * @return string
     */
    function aes_decrypt(string $encrypted, string $key, string $iv): string {
        return openssl_decrypt(base64_decode($encrypted), "AES-128-CBC", $key, 0, $iv);
    }
}

if (!function_exists('rsa_encrypt')) {
    /**
     * Notes: RSA数据加密
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:10
     * @param string $data [源数据]
     * @param string $publicKey [公钥]
     * @return string
     */
    function rsa_encrypt(string $data, string $publicKey): string {
        openssl_public_encrypt($data, $encrypted, $publicKey);
        return base64_encode($encrypted);
    }
}

if (!function_exists('rsa_decrypt')) {
    /**
     * Notes: RSA数据解密
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:13
     * @param string $encrypted [密文]
     * @param string $privateKey [私钥]
     * @return string
     */
    function rsa_decrypt(string $encrypted, string $privateKey): string {
        openssl_private_decrypt(base64_decode($encrypted), $decrypted, $privateKey);
        return $decrypted;
    }
}

if (!function_exists('rsa_sign')) {
    /**
     * Notes: RSA数据签名
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:14
     * @param string $data [源数据]
     * @param string $privateKey [私钥]
     * @return string
     */
    function rsa_sign(string $data, string $privateKey): string {
        openssl_sign($data, $signature, $privateKey);
        return base64_encode($signature);
    }
}

if (!function_exists('rsa_sign_verify')) {
    /**
     * Notes: RSA数据验签
     * Author: Uncle-L
     * Date: 2021/12/27
     * Time: 16:15
     * @param string $data [源数据]
     * @param string $sign [签名]
     * @param string $publicKey [公钥]
     * @return bool
     */
    function rsa_sign_verify(string $data, string $sign, string $publicKey): bool {
        return (bool)openssl_verify($data, base64_decode($sign), $publicKey);
    }
}

if (!function_exists('request_server')) {
    /**
     * 获取请求的server参数
     * @access public
     * @param string $name 数据名称
     * @param string $default 默认值
     * @return mixed
     */
    function request_server(string $name = '', string $default = '') {
        if ($name == '') {
            return $_SERVER;
        } else {
            $name = strtoupper($name);
        }
        return $_SERVER[$name] ?? $default;
    }
}

if (!function_exists('request_is_ssl')) {
    /**
     * 获取请求是否ssl
     * @return bool
     */
    function request_is_ssl(): bool {
        static $isSSL;
        if (!isset($isSSL)) {
            $isSSL = false;
            if (request_server('HTTPS') && ('1' == request_server('HTTPS') || 'on' == strtolower(request_server('HTTPS')))) {
                $isSSL = true;
            } elseif ('https' == request_server('REQUEST_SCHEME')) {
                $isSSL = true;
            } elseif ('443' == request_server('SERVER_PORT')) {
                $isSSL = true;
            } elseif ('https' == request_server('HTTP_X_FORWARDED_PROTO')) {
                $isSSL = true;
            }
        }
        return $isSSL;
    }
}

if (!function_exists('request_scheme')) {
    /**
     * 获取请求URL地址中的scheme参数
     * @return string
     */
    function request_scheme(): string {
        return request_is_ssl() ? 'https' : 'http';
    }
}

if (!function_exists('request_host')) {
    /**
     * 获取请求的host
     * @param bool $exclude_port 排除端口
     * @return string
     */
    function request_host(bool $exclude_port = false): string {
        static $host;
        if (!isset($host)) {
            $host = strval(request_server('HTTP_X_FORWARDED_HOST') ?: request_server('HTTP_HOST'));
        }
        return true === $exclude_port && strpos($host, ':') ? strstr($host, ':', true) : $host;
    }
}

if (!function_exists('request_domain')) {
    /**
     * 获取请求的包含协议的域名
     * @param bool $exclude_port 排除端口
     * @return string
     */
    function request_domain(bool $exclude_port = false): string {
        return request_scheme() . '://' . request_host($exclude_port);
    }
}

if (!function_exists('request_base_url')) {
    /**
     * 获取请求的URL 不含QUERY_STRING
     * @param bool $complete 是否包含完整域名
     * @return string
     */
    function request_base_url(bool $complete = false): string {
        static $baseUrl;
        if (!isset($baseUrl)) {
            $str = request_url();
            $baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str;
        }
        return $complete ? request_domain() . $baseUrl : $baseUrl;
    }
}

if (!function_exists('request_url')) {
    /**
     * 获取请求的完整URL 包括QUERY_STRING
     * @param bool $complete 是否包含完整域名
     * @return string
     */
    function request_url(bool $complete = false): string {
        static $baseUrl;
        if (!isset($baseUrl)) {
            if (request_server('HTTP_X_REWRITE_URL')) {
                $url = request_server('HTTP_X_REWRITE_URL');
            } elseif (request_server('REQUEST_URI')) {
                $url = request_server('REQUEST_URI');
            } elseif (request_server('ORIG_PATH_INFO')) {
                $url = request_server('ORIG_PATH_INFO') . (!empty(request_server('QUERY_STRING')) ? '?' . request_server('QUERY_STRING') : '');
            } elseif (isset($_SERVER['argv'][1])) {
                $url = $_SERVER['argv'][1];
            } else {
                $url = '';
            }
        }
        return $complete ? request_domain() . $url : $url;
    }
}

if (!function_exists('request_ip')) {
    /**
     * 获取请求的客户端IP地址
     * @return string
     */
    function request_ip(): string {
        static $ip;
        if (!isset($ip)) {
            $ip = request_server('REMOTE_ADDR', '');
            if (!is_valid_ip($ip)) {
                $ip = '0.0.0.0';
            }
        }
        return $ip;
    }
}

if (!function_exists('request_method')) {
    /**
     * 获取当前的请求类型
     * @param bool $origin 是否获取原始请求类型
     * @return string
     */
    function request_method(bool $origin = false): string {
        static $method;
        $method = $method ?: [];
        if (!isset($method[$origin])) {
            if (!$origin && ($_method = request_server('HTTP_X_HTTP_METHOD_OVERRIDE'))) {
                $method[$origin] = strtoupper($_method);
            } else {
                $method[$origin] = strtoupper(request_server('REQUEST_METHOD') ?: 'GET');
            }
        }
        return $method[$origin];
    }
}

if (!function_exists('parse_sql_file')) {
    /**
     * 解析sql文件得到sql语句
     * @param string $sql_file_path sql文件路径
     * @param bool $arr_sql 是否获取sql数组，还是获取一条语句
     * @param string|array $search 要查找的值
     * @param string|array $replace 替换 search 中的值的值
     * @return string|string[]  除去注释之后的sql语句数组或一条语句
     */
    function parse_sql_file(string $sql_file_path, bool $arr_sql, $search = '', $replace = '') {
        $res = $arr_sql ? [] : '';
        if (!file_exists($sql_file_path)) return $res;
        $sql = file_get_contents($sql_file_path);
        if (!$sql) return $res;
        // 纯sql内容
        $pureSql = [];
        // 多行注释标记
        $comment = false;
        // 按行分割，兼容多个平台
        $sql = str_replace(["\r\n", "\r"], "\n", $sql);
        $sql = explode("\n", trim($sql));

        // 循环处理每一行
        foreach ($sql as $key => $line) {
            // 跳过空行
            if ($line == '') continue;

            // 跳过以#或者--开头的单行注释
            if (preg_match("/^(#|--)/", $line)) continue;
            // 跳过以/**/包裹起来的单行注释
            if (preg_match("/^\/\*(.*?)\*\//", $line)) continue;

            // 多行注释开始
            if (substr($line, 0, 2) == '/*') {
                $comment = true;
                continue;
            }
            // 多行注释结束
            if (substr($line, -2) == '*/') {
                $comment = false;
                continue;
            }
            // 多行注释没有结束，继续跳过
            if ($comment) continue;

            // 替换表前缀
            if ($search && $replace) {
                $line = str_replace($search, $replace, $line);
            }
            if ($line == 'BEGIN;' || $line == 'COMMIT;') continue;
            // sql语句
            array_push($pureSql, $line);
        }
        // 只返回一条语句
        if (!$arr_sql) {
            return implode('', $pureSql);
        }
        // 以数组形式返回sql语句
        $pureSql = implode("\n", $pureSql);
        $pureSql = explode(";\n", $pureSql);
        return $pureSql;
    }
}

if (!function_exists('http_request')) {
    /**
     * http请求
     * @param string $url
     * @param string $data
     * @param array $header
     * @param array $cert
     * @param int $timeout
     * @param string $method
     * @return bool|string
     */
    function http_request(string $url, $data = '', $header = [], $cert = [], $timeout = 5, $method = '') {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        if (!empty($data)) {
            curl_setopt($curl, CURLOPT_POST, 1);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }
        curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
        if ($cert) {
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($curl, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($curl, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($curl, CURLOPT_SSLCERT, $cert['sslCertPath']);
            curl_setopt($curl, CURLOPT_SSLKEY, $cert['privateKeyPath']);
        } else {
            if (substr($url, 0, 5) == 'https') {
                curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
                curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
            }
        }
        if (!empty($header)) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        }
        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        if ($method) {
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        $output = curl_exec($curl);
        $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        $error = curl_error($curl);
        curl_close($curl);

        if ($http_code != 200 && $http_code != 201 && $http_code != 204) {
            $errMsg = '';
            if ($error) $errMsg = $error;
            if ($output) $errMsg .= ($errMsg ? '-' : '') . $output;
            $errMsg = $errMsg ?: "外部服务异常";
            throw new \RuntimeException($errMsg, 500);
        }
        return $output;
    }
}

if (!function_exists('ip_location')) {
    /**
     * ip归属地
     * @param string $ip
     * @return string
     */
    function ip_location(string $ip): string {
        if (!$ip) return '';
        try {
            $api = "https://qifu-api.baidubce.com/ip/geo/v1/district?ip={$ip}";
            $output = http_request($api);
            $res = json_decode($output, true);
            if (!$res) return '';
            $location = ($res['data']['prov'] ?? '') . ($res['data']['city'] ?? '') . ($res['data']['district'] ?? '');
            return trim($location);
        } catch (\Throwable $e) {
            return '';
        }
    }
}

if (!function_exists('is_request')) {
    /**
     * 当前是否为http请求
     * @return bool
     */
    function is_request(): bool {
        return isset($_SERVER['HTTP_HOST']) || isset($_SERVER['REQUEST_METHOD']);
    }
}
